﻿using System;
using System.Collections.Generic;
using Pfz.Caching;
using Pfz.Extensions;

namespace Pfz.DynamicObjects.CloneToModifyModel
{
	/// <summary>
	/// Represents a local (non-shared) cache of Ctm objects.
	/// </summary>
	/// <typeparam name="TKey">The type of the key of the cache.</typeparam>
	/// <typeparam name="TItem">The type of the items in the cache.</typeparam>
	public sealed class CtmLocalCache<TKey, TItem>
	where
		TItem: class, ICtmCacheable<TKey>
	{
		private readonly CtmGlobalCache<TKey, TItem> _globalCache;
		private readonly Dictionary<TKey, KeyValuePair<TItem, TItem>> _dictionary = new Dictionary<TKey, KeyValuePair<TItem, TItem>>();
		private readonly HashSet<TKey> _updated = new HashSet<TKey>();
		private readonly Dictionary<TKey, TItem> _dictionaryReadOnly = new Dictionary<TKey, TItem>();
		private readonly List<TItem> _newReadOnly = new List<TItem>();
		
		internal CtmLocalCache(CtmGlobalCache<TKey, TItem> globalCache)
		{
			_globalCache = globalCache;
		}
		
		/// <summary>
		/// Clears the cache of modifiable items. Read-only items
		/// are kept.
		/// </summary>
		public void Clear()
		{
			_dictionary.Clear();
			_updated.Clear();
		}
		
		/// <summary>
		/// Clears both modifiable and read-only items.
		/// </summary>
		public void ClearAll()
		{
			_dictionary.Clear();
			_updated.Clear();
			
			_dictionaryReadOnly.Clear();
			_newReadOnly.Clear();
		}
		
		/// <summary>
		/// Gets an item by key. If it is not found in the local cache,
		/// the global cache is searched. May return null if it is
		/// simple not in the cache at all.
		/// </summary>
		public TItem Get(TKey key)
		{
			TItem result;
			KeyValuePair<TItem, TItem> pair;
			if (_dictionary.TryGetValue(key, out pair))
			{
				result = pair.Value;
				GCUtils.KeepAlive(result);
				return result;
			}
				
			if (_dictionaryReadOnly.TryGetValue(key, out result))
			{
				GCUtils.KeepAlive(result);
				return result;
			}
				
			result = _globalCache._Get(key);
			if (result != null)
			{
				GCUtils.KeepAlive(result);
				
				bool isNonUpdatable = result.GetCtmType().IsNonUpdatable;
				if (isNonUpdatable)
					_dictionaryReadOnly.Add(key, result);
				else
				{
					pair = new KeyValuePair<TItem, TItem>(result, result);
					_dictionary.Add(key, pair);
				}
			}
			
			return result;
		}
		
		/// <summary>
		/// Merges this cache with the global cache.
		/// Conflicting items (like items that may be updated by other threads) are
		/// simple removed from the cache.
		/// </summary>
		public void Merge()
		{
			if (_updated.Count > 0)
			{
				_globalCache._modifiableItemsLock.EnterWriteLock();
				try
				{
					var globalModifiable = _globalCache._modifiableItems;
					foreach(var key in _updated)
					{
						var pair = _dictionary[key];
						var oldItem = pair.Key;
						var item = pair.Value;
						var other = globalModifiable[key];
						
						if (other == null || other.Equals(oldItem))
						{
							globalModifiable[key] = item;
							_dictionary[key] = new KeyValuePair<TItem, TItem>(item, item);
						}
						else
						{
							globalModifiable.Remove(key);
							_dictionary.Remove(key);
						}
					}
				}
				finally
				{
					_globalCache._modifiableItemsLock.ExitWriteLock();
				}
				
				_updated.Clear();
			}
			
			var globalReadOnly = _globalCache._readOnlyItems;
			foreach(var item in _newReadOnly)
			{
				var key = item.GetCacheKey();
				
				var other = globalReadOnly.GetOrCreateValue(key, (k) => item);
				if (other != item)
				{
					if (!item.Equals(other))
						throw new InvalidOperationException("The cache is corrupted. There are two different non-updatable items in the cache.");
						
					_dictionaryReadOnly[key] = other;
				}
			}
			_newReadOnly.Clear();
		}
		
		/// <summary>
		/// Puts the given item in the cache and returns the read-only
		/// version of it (if it is not already read-only). Future updates 
		/// before clearing the cache must use the given return as the source.
		/// </summary>
		public TItem Set(TItem item)
		{
			if (item == null)
				throw new ArgumentNullException("item");
				
			var key = item.GetCacheKey();
			TItem oldItem;
			bool isNonUpdatable = item.GetCtmType().IsNonUpdatable;
			if (isNonUpdatable)
			{
				if (!_dictionaryReadOnly.TryGetValue(key, out oldItem))
				{
					item = item.AsReadOnly(false);
						
					_dictionaryReadOnly.Add(key, item);
					_newReadOnly.Add(item);
					return item;
				}
				
				if (!item.Equals(oldItem))
					throw new InvalidOperationException("Another non-updatable item with different values was already put into the cache.");
					
				return oldItem;
			}
			
			KeyValuePair<TItem, TItem> pair;	
			if (_dictionary.TryGetValue(key, out pair))
			{
				var oldInstance = item.GetOldInstance();
				if (oldInstance == null)
					throw new InvalidOperationException("You are putting a new item in the cache but there is one already there. If you are updating, create a modifiable clone from the read-only one.");
			
				oldItem = pair.Value;
				if (oldInstance != oldItem && !oldInstance.Equals(oldItem))
					throw new InvalidOperationException("The actual item does not has an old item compatible with the one in the cache. There are possible problems with your transactions.");
					
				GCUtils.Expire(oldItem);
			}

			item = item.AsReadOnly(false);
			pair = new KeyValuePair<TItem,TItem>(pair.Key, item);
			_dictionary[key] = pair;
			_updated.Add(key);
			return item;
		}
	}
}
